# vPresent is an immersive presentation creation and display application.
# Copyright (C) 2007-2009 by Priority 5 Holdings, Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

from PyQt4 import QtCore, QtGui
import types

import prefsui
import gui.cluster.node
import gui.cluster.display
import gui.util.environment as environ
import gui.util.settings

def makeNodeItem(itemText):
   new_item = QtGui.QListWidgetItem(itemText)
   item_flags = QtCore.Qt.ItemIsSelectable  | \
                QtCore.Qt.ItemIsEditable    | \
                QtCore.Qt.ItemIsDragEnabled | \
                QtCore.Qt.ItemIsEnabled
   new_item.setFlags(item_flags)

   return new_item

def makePipeItem(itemText):
   new_item = QtGui.QListWidgetItem(itemText)
   item_flags = QtCore.Qt.ItemIsSelectable  | \
                QtCore.Qt.ItemIsEditable    | \
                QtCore.Qt.ItemIsDragEnabled | \
                QtCore.Qt.ItemIsEnabled
   new_item.setFlags(item_flags)

   return new_item

class ProjectorTableModel(QtCore.QAbstractTableModel):
   def __init__(self, projector, undoStack, parent = None):
      QtCore.QAbstractTableModel.__init__(self, parent)
      self.mProjector = projector
      self.mUndoStack = undoStack

   def rowCount(self, parent = QtCore.QModelIndex()):
      count = 0
      if self.mProjector is not None:
         count = self.mProjector.dataCount()

      return count

   def columnCount(self, parent = QtCore.QModelIndex()):
      return 2

   def headerData(self, section, orientation, role = QtCore.Qt.DisplayRole):
      if orientation == QtCore.Qt.Vertical:
         return QtCore.QVariant()
      elif role == QtCore.Qt.EditRole or QtCore.Qt.DisplayRole == role:
         if 0 == section:
            return QtCore.QVariant("Name")
         elif 1 == section:
            return QtCore.QVariant("Value")

      return QtCore.QVariant()

   def flags(self, index):
      flags = None

      if index.isValid():
         flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
         if 1 == index.column():
            flags |= QtCore.Qt.ItemIsEditable

      return flags

   def data(self, index, role):
      if self.mProjector is not None:
         if QtCore.Qt.EditRole == role or QtCore.Qt.DisplayRole == role:
            return self.mProjector.data(index, role)

      if role == QtCore.Qt.UserRole:
         print "Qt.UserRole!"
         return None

      return QtCore.QVariant()

   def setData(self, index, value, role):
      result = False

      if self.mProjector is not None:
         if role == QtCore.Qt.EditRole:
            if self.mProjector.data(index, role) != value:
               self.mUndoStack.push(
                  ChangeProjectorParam(self, self.mProjector, index, value,
                                       role, self.mProjector.getDesc(index))
               )
               result = True

      return result

   def param(self, index, role):
      p = None
      if self.mProjector is not None:
         p = self.mProjector.getParam(index)

      return p

class ProjectorParamDelegate(QtGui.QItemDelegate):
   '''
   '''

   def __init__(self, parent = None):
      QtGui.QItemDelegate.__init__(self, parent)

   def createEditor(self, parent, option, index):
      e = QtGui.QItemDelegate.createEditor(self, parent, option, index)

      # If the editor is a QDoubleSpinBox, set its minimum based on the
      # parameter that will be edited.
      if isinstance(e, QtGui.QDoubleSpinBox) or isinstance(e, QtGui.QSpinBox):
         # XXX: This is a bad design. It requires that the data model be
         # ProjectorTableModel.
         param = index.model().param(index, QtCore.Qt.UserRole)

         if param is not None:
            if param.hasMinimum():
               e.setMinimum(param.getMinimum())
            if param.hasMaximum():
               e.setMaximum(param.getMaximum())

      return e

class PrefsEditor(QtGui.QDialog, prefsui.Ui_Dialog):
   sDefaultProtocolType = 'Multicast'
   sProtocolTypes = {
      'Multicast'     : 'Multicast',
      'Stream Socket' : 'StreamSock'
   }

   def __init__(self, env, parent = None):
      QtGui.QDialog.__init__(self, parent)

      # Core data members.
      self.mEnvironment  = env
      self.mCluster      = env.getCluster()
      self.mClusterModel = None
      self.mPipeModel    = None
      self.mCurrentNode  = None

      # Miscellaneous stuff.
      self.mTableDelegate = None

      # Ensure that we have cluster settings ready to go before we do any
      # editing.
      settings = env.getSettings()
      if not settings.has_key('cluster'):
         settings.add('cluster', site = True)

      assert settings.has_key('cluster')

      self.mClusterSettingsHandler = \
         ClusterSettingsChangeHandler(self.mCluster, settings['cluster'])
      self.mNodeSettingsHandler = None
      self.mPipeSettingsHandler = None

      self.setupUi(self)

      # Set up the model for self.clusterNodeList.
      self.mClusterModel = ClusterModel(self.mCluster, self.undoStack)
      self.clusterNodeList.setModel(self.mClusterModel)

      self.connect(self.mClusterModel,
                   QtCore.SIGNAL("rowsInserted(QModelIndex,int,int)"),
                   self.onNodeInserted)
      self.connect(self.mClusterModel,
                   QtCore.SIGNAL("rowsRemoved(QModelIndex,int,int)"),
                   self.onNodeRemoved)
      self.connect(
         self.clusterNodeList.selectionModel(),
         QtCore.SIGNAL("selectionChanged(QItemSelection,QItemSelection)"),
         self.onNodeSelectionChange
      )

      index = self.mClusterModel.index(0)
      if index.isValid():
         self.clusterNodeList.selectionModel().setCurrentIndex(
            index, QtGui.QItemSelectionModel.ClearAndSelect
         )

      # Fill in the saved backup interval.
      default_backup_interval = 60
      backup_interval = settings.getValue('general/backup_interval', default_backup_interval)
      try:
         self.backupInterval.setValue(int(backup_interval))
      except:
         # If converting to an int fails, use default value.
         self.backupInterval.setValue(default_backup_interval)

      self.connect(self.backupInterval,
                   QtCore.SIGNAL("valueChanged(int)"),
                   self.onBackupIntervalChanged)

   def setupUi(self, widget):
      prefsui.Ui_Dialog.setupUi(self, widget)

      # Disable this check box until the render server supports borderless
      # windows.
      self.withBorderCheckBox.setEnabled(False)

      self.buttonBox.button(QtGui.QDialogButtonBox.Save).setEnabled(False)

      triggers = QtGui.QAbstractItemView.DoubleClicked   | \
                 QtGui.QAbstractItemView.CurrentChanged  | \
                 QtGui.QAbstractItemView.SelectedClicked | \
                 QtGui.QAbstractItemView.EditKeyPressed  | \
                 QtGui.QAbstractItemView.AnyKeyPressed
      self.projSettingsTable.setEditTriggers(triggers)
      self.projSettingsTable.setTabKeyNavigation(True)

      # NOTE: We do not need an undo group because this dialog box will have
      # only one undo stack.
      self.undoStack = QtGui.QUndoStack(widget)
      self.connect(self.undoStack, QtCore.SIGNAL("cleanChanged(bool)"),
                   self.cleanStateChanged)

      undo_action = self.undoStack.createUndoAction(widget)
      undo_action.setIcon(QtGui.QIcon(":/vpresent/gui/images/undo32.png"))
      undo_action.setShortcut(self.tr("Ctrl+Z"))

      redo_action = self.undoStack.createRedoAction(widget)
      redo_action.setIcon(QtGui.QIcon(":/vpresent/gui/images/redo32.png"))
      redo_action.setShortcut(self.tr("Ctrl+Shift+Z"))

      # Create a tool bar to contain the buttons for actions related to
      # editing preferences.
      toolbar = QtGui.QToolBar(widget)
      toolbar.addAction(undo_action)
      toolbar.addAction(redo_action)
      toolbar.addSeparator()
      toolbar.addAction(self.actionCut)
      toolbar.addAction(self.actionCopy)
      toolbar.addAction(self.actionPaste)

      # Put the tool bar at the top of the dialog box.
      self.vboxlayout.insertWidget(0, toolbar)

      self.rotMultiplierEditor.setValidator(QtGui.QDoubleValidator(widget))

      settings = self.mEnvironment.getSettings()

      # Fill in the rotation multiplier.
      default_rot_mult = '1.0'
      rot_mult = settings.getValue('navigation/rot_mult', default_rot_mult)
      self.rotMultiplierEditor.setText(rot_mult.strip())

      self.connect(self.rotMultiplierEditor,
                   QtCore.SIGNAL("editingFinished()"),
                   self.onRotMultiplierChanged)

      # XXX: Having a hard-coded maximum like this is not great, but somewhere,
      # this has to be set in order for the integer validator to work.
      max_resolution = 10240

      # Create integer validators for the dimensions and position of windows.

      # For the dimensions, we do not want the size of the window to be less
      # than 1.
      # XXX: This minimum value is not working for some reason. It still
      # allows the user to enter 0!
      dim_validator = QtGui.QIntValidator(1, max_resolution, widget)
      self.windowWidthEditor.setValidator(dim_validator)
      self.windowHeightEditor.setValidator(dim_validator)

      # The X Window System allows the use of negative values to make window
      # positions relative to the opposite side of the screen. I need to
      # verify what Mac OS X and Windows do with this sort of information.
      pos_validator = QtGui.QIntValidator(-max_resolution, max_resolution,
                                          widget)
      self.windowPosXEditor.setValidator(pos_validator)
      self.windowPosYEditor.setValidator(pos_validator)

      for k, v in self.sProtocolTypes.iteritems():
         self.commTypeComboBox.addItem(self.tr(k), QtCore.QVariant(v))

      protocol = settings.get('network/protocol')

      if protocol is None:
         attrs = { 'type' : self.sDefaultProtocolType }
         protocol = gui.util.settings.Setting(name = 'protocol',
                                              attributes = attrs)
         protocol.addChild(gui.util.settings.Setting(name = 'address',
                                                     value = ''))
         protocol.addChild(gui.util.settings.Setting(name = 'port',
                                                     value = ''))

         network = settings.get('network')
         if network is None:
            settings.add(key = 'network', site = True)
            network = settings.get('network')

         network.addChild(protocol)

      protocol_type = protocol['type']

      i = 0
      found = False
      while i < self.commTypeComboBox.count():
         d = self.commTypeComboBox.itemData(i)
         if d.toString() == protocol_type:
            self.commTypeComboBox.setCurrentIndex(i)
            found = True
            break

         i += 1

      if not found:
         warning_text = "Invalid network protocol '%s' in preferences!" % \
                           protocol_type
         QtGui.QMessageBox.warning(self, "Unknown Network Protocol",
                                   warning_text,
                                   QtGui.QMessageBox.RestoreDefaults | \
                                      QtGui.QMessageBox. Default     | \
                                      QtGui.QMessageBox.Escape,
                                   QtGui.QMessageBox.NoButton,
                                   QtGui.QMessageBox.NoButton)
         protocol['type'] = self.sDefaultProtocolType

      address = protocol.getChild('address')
      port    = protocol.getChild('port')

      if address is not None:
         try:
            self.serviceAddrEditor.setText(address.value.strip())
         except:
            pass

      if port is not None:
         try:
            self.servicePortEditor.setValue(int(port.value.strip()))
         except:
            pass

      is_multicast = protocol['type'] == 'Multicast'
      self.serviceAddrEditor.setEnabled(is_multicast)
      self.servicePortEditor.setEnabled(is_multicast)

      self.nodePortEditor.setValue(gui.cluster.node.Node.getDefaultPort())

      self.connect(self.buttonBox, QtCore.SIGNAL("clicked(QAbstractButton*)"),
                   self.onSave)

      self.connect(self.commTypeComboBox,
                   QtCore.SIGNAL("currentIndexChanged(int)"),
                   self.onCommTypeChanged)

      self.connect(self.removeButton, QtCore.SIGNAL("clicked()"),
                   self.onRemoveNode)
      self.connect(self.nodeUpButton, QtCore.SIGNAL("clicked()"),
                   self.onMoveNodeUp)
      self.connect(self.nodeDownButton, QtCore.SIGNAL("clicked()"),
                   self.onMoveNodeDown)

      self.connect(self.nodeAddrEditor, QtCore.SIGNAL("editingFinished()"),
                   self.onNodeAddrChanged)
      self.connect(self.nodePortEditor, QtCore.SIGNAL("valueChanged(int)"),
                   self.onNodePortChanged)

      self.connect(self.addPipeButton, QtCore.SIGNAL("clicked()"),
                   self.onAddPipe)
      self.connect(self.removePipeButton, QtCore.SIGNAL("clicked()"),
                   self.onRemovePipe)
      self.connect(self.pipeUpButton, QtCore.SIGNAL("clicked()"),
                   self.onMovePipeUp)
      self.connect(self.pipeDownButton, QtCore.SIGNAL("clicked()"),
                   self.onMovePipeDown)

      self.connect(self.fullScreenCheckBox,
                   QtCore.SIGNAL("stateChanged(int)"),
                   self.onFullScreenChanged)
      self.connect(self.alwaysOnTopCheckBox,
                   QtCore.SIGNAL("stateChanged(int)"),
                   self.onAlwaysOnTopChanged)
      self.connect(self.withBorderCheckBox,
                   QtCore.SIGNAL("stateChanged(int)"), self.onBorderChanged)
      self.connect(self.stereoCheckBox, QtCore.SIGNAL("stateChanged(int)"),
                   self.onStereoChanged)

      self.connect(self.windowWidthEditor, QtCore.SIGNAL("editingFinished()"),
                   self.onWindowWidthChanged)
      self.connect(self.windowHeightEditor, QtCore.SIGNAL("editingFinished()"),
                   self.onWindowHeightChanged)
      self.connect(self.windowPosXEditor, QtCore.SIGNAL("editingFinished()"),
                   self.onWindowPosXChanged)
      self.connect(self.windowPosYEditor, QtCore.SIGNAL("editingFinished()"),
                   self.onWindowPosYChanged)
      self.connect(self.screenIdEditor, QtCore.SIGNAL("editingFinished()"),
                   self.onScreenIdChanged)

      self.connect(self.projParamTypeCB,
                   QtCore.SIGNAL("currentIndexChanged(int)"),
                   self.onProjectorTypeChanged)

      self.connect(self.addButton, QtCore.SIGNAL("clicked()"), self.onAddNode)

   def cleanStateChanged(self, clean):
      can_save = not clean
      self.buttonBox.button(QtGui.QDialogButtonBox.Save).setEnabled(can_save)

   def onSave(self, button):
      if self.buttonBox.standardButton(button) == QtGui.QDialogButtonBox.Save:
         self.mEnvironment.getSettings().save()

   def onBackupIntervalChanged(self, val):
      settings = self.mEnvironment.getSettings()
      
      if not settings.has_key('general/backup_interval'):
         settings.add('general/backup_interval')

      self.undoStack.push(ChangeBackupInterval(settings, self.backupInterval, val))

   def onRotMultiplierChanged(self):
      settings = self.mEnvironment.getSettings()
      if not settings.has_key('navigation/rot_mult'):
         s = settings.add('navigation/rot_mult')

      self.undoStack.push(ChangeRotationMultiplier(self.rotMultiplierEditor,
                                                   settings))

   def onCommTypeChanged(self, index):
      new_type = str(self.commTypeComboBox.itemData(index).toString())
      settings = self.mEnvironment.getSettings()
      self.undoStack.push(ChangeProtocolType(self.commTypeComboBox, settings,
                                             new_type))

      is_multicast = new_type == 'Multicast'
      self.serviceAddrEditor.setEnabled(is_multicast)
      self.servicePortEditor.setEnabled(is_multicast)

   def onAddNode(self):
      self.undoStack.push(AddNode(self.mClusterModel, self.mCluster))

      # Begin editing the new node item in the list.
      # NOTE: This works because onNodeInserted() changes the selected entry
      # in self.clusterNodeList.
      self.clusterNodeList.edit(self.clusterNodeList.currentIndex())

   def onRemoveNode(self):
      index = self.clusterNodeList.currentIndex()
      row   = index.row()
      assert row >= 0
      node = self.mClusterModel.data(index, QtCore.Qt.UserRole)
      self.undoStack.push(RemoveNode(self.mClusterModel, row, node))

      # It is important that we call this here so that the up/down buttons
      # are updated based on the state of self.clusterNodeList after the
      # removal has completed. Relying on it being called as a result of
      # selection changing has the wrong effect because the selection changes
      # before the removal completes.
      self._updateNodeUpDownButtons()

   def onNodeInserted(self, index, start, end):
      assert start == end
      assert self.mClusterModel is not None

      self.clusterNodeList.selectionModel().setCurrentIndex(
         self.mClusterModel.index(start),
         QtGui.QItemSelectionModel.ClearAndSelect
      )

   def onNodeRemoved(self, idnex, start, end):
      assert start == end
      assert self.mClusterModel is not None

      if start >= self.mClusterModel.rowCount():
         start = self.mClusterModel.rowCount() - 1

      if start >= 0:
         self.clusterNodeList.selectionModel().setCurrentIndex(
            self.mClusterModel.index(start),
            QtGui.QItemSelectionModel.ClearAndSelect
         )

   def onNodeSelectionChange(self, selected, deselected):
      node_selected = not selected.isEmpty()

      self.nodeAddrEditor.blockSignals(True)
      self.nodePortEditor.blockSignals(True)

      # Whenever a node selection changes and we already have a pipe model
      # (i.e., a node with graphics pipes defined), we have to clear out the
      # current state of self.mPipeModel and self.pipeList.
      if self.mPipeModel is not None:
         self.pipeList.selectionModel().clear()

         self.disconnect(self.mPipeModel,
                         QtCore.SIGNAL("rowsInserted(QModelIndex,int,int)"),
                         self.onPipeInserted)
         self.disconnect(self.mPipeModel,
                         QtCore.SIGNAL("rowsRemoved(QModelIndex,int,int)"),
                         self.onPipeRemoved)
         self.disconnect(
            self.pipeList.selectionModel(),
            QtCore.SIGNAL("selectionChanged(QItemSelection,QItemSelection)"),
            self.onPipeSelectionChange
         )


      if self.mNodeSettingsHandler is not None:
         self.mNodeSettingsHandler.disconnectSlots()
         self.mNodeSettinsgHandler = None

      if node_selected:
         index = self.clusterNodeList.currentIndex()
         assert index.row() >= 0
         current_node = self.mClusterModel.data(index, QtCore.Qt.UserRole)
         self.nodeAddrEditor.setText(current_node.getAddress())
         self.nodePortEditor.setValue(current_node.getPort())
         self.mCurrentNode = current_node

         settings      = self.mEnvironment.getSettings()
         node_settings = settings['cluster'].getChildAtIndex(index.row())
         self.mNodeSettingsHandler = NodeSettingsChangeHandler(current_node,
                                                               node_settings)

         self.mPipeModel = PipeModel(current_node.getPipes(), self.undoStack)
         self.pipeList.setModel(self.mPipeModel)

         self.connect(self.mPipeModel,
                      QtCore.SIGNAL("rowsInserted(QModelIndex,int,int)"),
                      self.onPipeInserted)
         self.connect(self.mPipeModel,
                      QtCore.SIGNAL("rowsRemoved(QModelIndex,int,int)"),
                      self.onPipeRemoved)
         self.connect(
            self.pipeList.selectionModel(),
            QtCore.SIGNAL("selectionChanged(QItemSelection,QItemSelection)"),
            self.onPipeSelectionChange
         )

         index = self.mPipeModel.index(0)
         if index.isValid():
            self.pipeList.selectionModel().setCurrentIndex(
               index, QtGui.QItemSelectionModel.ClearAndSelect
            )
      else:
         self.mCurrentNode = None
         self.nodeAddrEditor.clear()
         self.nodePortEditor.setValue(gui.cluster.node.Node.getDefaultPort())

         self.pipeList.setModel(None)
         self.mPipeModel = None

      self.nodeAddrEditor.blockSignals(False)
      self.nodePortEditor.blockSignals(False)

      self.removeButton.setEnabled(node_selected)
      self.nodeAddrEditor.setEnabled(node_selected)
      self.nodePortEditor.setEnabled(node_selected)
      self.pipeList.setEnabled(node_selected)
      self.addPipeButton.setEnabled(node_selected)
      self._updateNodeUpDownButtons()

   def onMoveNodeUp(self):
      row = self.clusterNodeList.selectionModel().currentIndex().row()
      self.undoStack.push(MoveNode(self.mClusterModel, row, row - 1,
                                   self.mCluster[row]))

   def onMoveNodeDown(self):
      row = self.clusterNodeList.selectionModel().currentIndex().row()
      self.undoStack.push(MoveNode(self.mClusterModel, row, row + 1,
                                   self.mCluster[row]))

   def onNodeAddrChanged(self):
      index = self.clusterNodeList.selectionModel().currentIndex()
      if index.isValid():
         row = index.row()
         self.undoStack.push(
            ChangeNodeAddress(self.nodeAddrEditor, self.mCluster[row],
                              str(self.nodeAddrEditor.text()))
         )

   def onNodePortChanged(self, newPort):
      index = self.clusterNodeList.selectionModel().currentIndex()
      if index.isValid():
         row = index.row()
         self.undoStack.push(ChangeNodePort(self.nodePortEditor,
                                            self.mCluster[row], newPort))

   def onAddPipe(self):
      self.undoStack.push(AddPipe(self.mPipeModel))

      # Begin editing the new node item in the list.
      # NOTE: This works because onPipeInserted() changes the selected entry
      # in self.pipeList.
      self.pipeList.edit(self.pipeList.currentIndex())

   def onRemovePipe(self):
      index = self.pipeList.currentIndex()
      row   = index.row()
      assert row >= 0
      pipe = self.mPipeModel.data(index, QtCore.Qt.UserRole)
      self.undoStack.push(RemovePipe(self.mPipeModel, row, pipe))

      # It is important that we call this here so that the up/down buttons
      # are updated based on the state of self.pipeList after the removal has
      # completed. Relying on it being called as a result of selection
      # changing has the wrong effect because the selection changes before the
      # removal completes.
      self._updatePipeUpDownButtons()

   def onPipeInserted(self, index, start, end):
      assert start == end
      assert self.mPipeModel is not None

      self.pipeList.selectionModel().setCurrentIndex(
         self.mPipeModel.index(start),
         QtGui.QItemSelectionModel.ClearAndSelect
      )

   def onPipeRemoved(self, idnex, start, end):
      assert start == end
      assert self.mPipeModel is not None

      if start >= self.mPipeModel.rowCount():
         start = self.mPipeModel.rowCount() - 1

      if start >= 0:
         self.pipeList.selectionModel().setCurrentIndex(
            self.mPipeModel.index(start),
            QtGui.QItemSelectionModel.ClearAndSelect
         )

   def onPipeSelectionChange(self, selected, deselected):
      pipe_selected = not selected.isEmpty()

      self.pipeInfoTabWidget.setEnabled(pipe_selected)

      self.screenIdEditor.blockSignals(True)
      self.fullScreenCheckBox.blockSignals(True)
      self.alwaysOnTopCheckBox.blockSignals(True)
      self.stereoCheckBox.blockSignals(True)
      self.withBorderCheckBox.blockSignals(True)
      self.windowWidthEditor.blockSignals(True)
      self.windowHeightEditor.blockSignals(True)
      self.windowPosXEditor.blockSignals(True)
      self.windowPosYEditor.blockSignals(True)
      self.projSettingsTable.blockSignals(True)

      if self.mPipeSettingsHandler is not None:
         self.mPipeSettingsHandler.disconnectSlots()
         self.mPipeSettingsHandler = None

      if pipe_selected:
         pipe, pipe_row = self._getCurrentPipe()
         window         = pipe.getWindow()

         node, node_row = self._getCurrentNode()
         settings       = self.mEnvironment.getSettings()
         node_settings  = settings.get('cluster').getChildAtIndex(node_row)
         pipe_settings  = \
            node_settings.getChild('pipes').getChildAtIndex(pipe_row)

         self.mPipeSettingsHandler = PipeSettingsChangeHandler(pipe,
                                                               pipe_settings)

         if window is None:
            answer = \
               QtGui.QMessageBox.question(
                  self, self.tr("Missing Window for Pipe"),
                  self.tr("Pipe %s has no window! Should one be created?" % \
                             pipe.getName()),
                  QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
               )

            if answer == QtGui.QMessageBox.Yes:
               window = gui.cluster.display.Window()
               pipe.setWindow(window)
            else:
               self.pipeInfoTabWidget.setEnabled(False)

         if window is not None:
            self.screenIdEditor.setText(pipe.getScreenID())

            if window.isFullScreen():
               self.fullScreenCheckBox.setCheckState(QtCore.Qt.Checked)
            else:
               self.fullScreenCheckBox.setCheckState(QtCore.Qt.Unchecked)

            if window.isAlwaysOnTop():
               self.alwaysOnTopCheckBox.setCheckState(QtCore.Qt.Checked)
            else:
               self.alwaysOnTopCheckBox.setCheckState(QtCore.Qt.Unchecked)

            if window.isStereo():
               self.stereoCheckBox.setCheckState(QtCore.Qt.Checked)
            else:
               self.stereoCheckBox.setCheckState(QtCore.Qt.Unchecked)

#            if window.hasBorder():
#               self.withBorderCheckBox.setCheckState(QtCore.Qt.Checked)
#            else:
#               self.withBorderCheckBox.setCheckState(QtCore.Qt.Unchecked)

            self.windowWidthEditor.setText(str(window.getWidth()))
            self.windowHeightEditor.setText(str(window.getHeight()))
            self.windowPosXEditor.setText(str(window.getX()))
            self.windowPosYEditor.setText(str(window.getY()))

            self._refreshWindowPropState()

         projector = pipe.getProjector()

         if projector is None:
            answer = \
               QtGui.QMessageBox.question(
                  self, self.tr("Missing Projector for Pipe"),
                  self.tr("Pipe %s has no projector! Should one be created?" % \
                             pipe.getName()),
                  QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
               )

            if answer == QtGui.QMessageBox.Yes:
               projector = gui.cluster.display.DefaultProjector.makeDefault()
               pipe.setProjector(projector)
            else:
               self.pipeInfoTabWidget.setEnabled(False)

         if projector is not None:
            # NOTE: It is critical that we hang onto a reference of the table
            # model that we create here. Otherwise, QTableView will not work
            # correctly.
            self.mTableModel = ProjectorTableModel(projector, self.undoStack)
            self.projSettingsTable.setModel(self.mTableModel)

            if self.mTableDelegate is None:
               self.mTableDelegate = ProjectorParamDelegate()

            self.projSettingsTable.setItemDelegate(self.mTableDelegate)

            # This has to be done every time the model changes.
            header = self.projSettingsTable.horizontalHeader()
            header.setResizeMode(0, QtGui.QHeaderView.Stretch)
            header.setResizeMode(1, QtGui.QHeaderView.Stretch)
      else:
         self.screenIdEditor.clear()
         self.fullScreenCheckBox.setCheckState(QtCore.Qt.Unchecked)
         self.alwaysOnTopCheckBox.setCheckState(QtCore.Qt.Unchecked)
         self.stereoCheckBox.setCheckState(QtCore.Qt.Unchecked)
         self.withBorderCheckBox.setCheckState(QtCore.Qt.Unchecked)
         self.windowWidthEditor.clear()
         self.windowHeightEditor.clear()
         self.windowPosXEditor.clear()
         self.windowPosYEditor.clear()
         self.projSettingsTable.setModel(None)
         self.mTableModel = None

      self.screenIdEditor.blockSignals(False)
      self.fullScreenCheckBox.blockSignals(False)
      self.alwaysOnTopCheckBox.blockSignals(False)
      self.stereoCheckBox.blockSignals(False)
      self.withBorderCheckBox.blockSignals(False)
      self.windowWidthEditor.blockSignals(False)
      self.windowHeightEditor.blockSignals(False)
      self.windowPosXEditor.blockSignals(False)
      self.windowPosYEditor.blockSignals(False)
      self.projSettingsTable.blockSignals(False)

      self.removePipeButton.setEnabled(pipe_selected)
      self._updatePipeUpDownButtons()

   def onMovePipeUp(self):
      current_row = self.pipeList.selectionModel().currentIndex().row()
      self.undoStack.push(MovePipe(self.mPipeModel, current_row,
                                   current_row - 1, self._getCurrentPipe()[0]))

   def onMovePipeDown(self):
      current_row = self.pipeList.selectionModel().currentIndex().row()
      self.undoStack.push(MovePipe(self.mPipeModel, current_row,
                                   current_row + 1, self._getCurrentPipe()[0]))

   def onFullScreenChanged(self, state):
      window = self._getCurrentPipe()[0].getWindow()
      self.undoStack.push(
         ToggleWindowProperty(self.fullScreenCheckBox, window.setFullScreen,
                              state != 0, 'Toggle Window Full Screen State')
      )

      self._refreshWindowPropState()

   def onAlwaysOnTopChanged(self, state):
      window = self._getCurrentPipe()[0].getWindow()
      self.undoStack.push(
         ToggleWindowProperty(self.alwaysOnTopCheckBox, window.setAlwaysOnTop,
                              state != 0, 'Toggle Window Always-On-Top State')
      )

   def onStereoChanged(self, state):
      window = self._getCurrentPipe()[0].getWindow()
      self.undoStack.push(ToggleWindowProeprty(self.stereoCheckBox,
                                               window.setStereo, state != 0,
                                               'Toggle Window Stereo State'))

   def onBorderChanged(self, state):
      window = self._getCurrentPipe()[0].getWindow()
      self.undoStack.push(
         ToggleWindowProperty(self.withBorderCheckBox, window.setHasBorder,
                              state != 0, 'Toggle Window Has-Border State')
      )

   def onWindowWidthChanged(self):
      window = self._getCurrentPipe()[0].getWindow()
      self.undoStack.push(
         ChangeWindowProperty(self.windowWidthEditor, window.setWidth,
                              int(str(self.windowWidthEditor.text())),
                              window.getWidth(),
                              'Change Window Width')
      )

   def onWindowHeightChanged(self):
      window = self._getCurrentPipe()[0].getWindow()
      self.undoStack.push(
         ChangeWindowProperty(self.windowHeightEditor, window.setHeight,
                              int(str(self.windowHeightEditor.text())),
                              window.getHeight(),
                              'Change Window Height')
      )

   def onWindowPosXChanged(self):
      window = self._getCurrentPipe()[0].getWindow()
      self.undoStack.push(
         ChangeWindowProperty(self.windowPosXEditor, window.setX,
                              int(str(self.windowPosXEditor.text())),
                              window.getX(),
                              'Change Window Horizontal Location')
      )

   def onWindowPosYChanged(self):
      window = self._getCurrentPipe()[0].getWindow()
      self.undoStack.push(
         ChangeWindowProperty(self.windowPosYEditor, window.setY,
                              int(str(self.windowPosYEditor.text())),
                              window.getY(),
                              'Change Window Vertical Location')
      )

   def onScreenIdChanged(self):
      pipe = self._getCurrentPipe()[0]
      self.undoStack.push(ChangePipeScreen(self.screenIdEditor, pipe,
                                           str(self.screenIdEditor.text())))

   def onProjectorTypeChanged(self, id):
      pass

   def _refreshWindowPropState(self):
      # If the checked state is unchecked, then we enable the window
      # properties relating to size and position. Otherwise, we do not want
      # the user to fiddle with those settings for full screen windows.
      enable_props = \
         self.fullScreenCheckBox.checkState() == QtCore.Qt.Unchecked

      self.windowWidthEditor.setEnabled(enable_props)
      self.windowHeightEditor.setEnabled(enable_props)
      self.windowPosXEditor.setEnabled(enable_props)
      self.windowPosYEditor.setEnabled(enable_props)
      # TODO: Uncomment this when the render server supports borderless
      # windows that are not full screen. This should be done when r484 is
      # backed out.
#      self.borderCheckBox.setEnabled(enable_props)

   def _getCurrentNode(self):
      node_index = self.clusterNodeList.currentIndex()

      if node_index.isValid():
         node_row = node_index.row()
         node     = self.mClusterModel.data(node_index, QtCore.Qt.UserRole)
      else:
         node_row = None
         node     = None

      return (node, node_row)

   def _getCurrentPipe(self):
      pipe_index = self.pipeList.currentIndex()

      if pipe_index.isValid():
         pipe_row = pipe_index.row()
         pipe     = self.mPipeModel.data(pipe_index, QtCore.Qt.UserRole)
      else:
         pipe_row = None
         pipe     = None

      return (pipe, pipe_row)

   def _updateNodeUpDownButtons(self):
      row = self.clusterNodeList.selectionModel().currentIndex().row()

      if row < 0:
         self.nodeUpButton.setEnabled(False)
         self.nodeDownButton.setEnabled(False)
      else:
         self.nodeUpButton.setEnabled(row > 0)
         self.nodeDownButton.setEnabled(row + 1 < self.mClusterModel.rowCount())

   def _updatePipeUpDownButtons(self):
      row = self.pipeList.selectionModel().currentIndex().row()

      if row < 0:
         self.pipeUpButton.setEnabled(False)
         self.pipeDownButton.setEnabled(False)
      else:
         self.pipeUpButton.setEnabled(row > 0)
         self.pipeDownButton.setEnabled(row + 1 < self.mPipeModel.rowCount())

   def _getSelectedNode(self):
      return None

class ClusterModel(QtCore.QAbstractListModel):
   def __init__(self, cluster, undoStack, parent = None):
      QtCore.QAbstractListModel.__init__(self, parent)

      self.mCluster   = cluster
      self.mUndoStack = undoStack

   def addNode(self, newNode):
      self.insertNode(len(self.mCluster), newNode)

   def insertNode(self, row, newNode):
      assert row >= 0

      self.beginInsertRows(QtCore.QModelIndex(), row, row)
      self.mCluster.insert(row, newNode)
      self.endInsertRows()

   def removeNode(self, rowOrNode):
      if rowOrNode is None:
         return

      if type(rowOrNode) == types.IntType:
         row  = rowOrNode
         node = self.mCluster[row]
      else:
         node = rowOrNode
         row  = self.mCluster.index(node)

      self.beginRemoveRows(QtCore.QModelIndex(), row, row)
      del self.mCluster[row]
      self.endRemoveRows()

#   def moveNode(self, rowOrNode, newRow):
#      if rowOrNode is None:
#         return
#
#      if type(rowOrNode) == types.IntType:
#         row = rowOrNode
#      else:
#         row = self.mClsuter.index(node)
#
#      self.mCluster.moveNode(row, newRow)

   def nodeChanged(self, node):
      if node in self.mCluster:
         row = self.mCluster.index(node)
         changed_index = self.index(row)
         self.emit(
            QtCore.SIGNAL("dataChanged(const QModelIndex &, const QModelIndex &)"),
            changed_index, changed_index
         )

   def onClusterChanged(self):
      self.emit(QtCore.SIGNAL("modelReset()"))

   def flags(self, index):
      default_flags  = QtCore.QAbstractListModel.flags(self, index)
      default_flags |= QtCore.Qt.ItemIsEditable

      if index.isValid():
         return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | \
                   default_flags
      else:
         return QtCore.Qt.ItemIsDropEnabled | default_flags

   def data(self, index, role = QtCore.Qt.DisplayRole):
      if index.isValid():
         node = self.mCluster[index.row()]

         if role == QtCore.Qt.UserRole:
            return node

         if node is not None:
            if role == QtCore.Qt.DecorationRole:
               return QtCore.QVariant()
            elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
               return QtCore.QVariant(str(node.getName()))

      return QtCore.QVariant()

   def setData(self, index, value, role):
      if not index.isValid() or index.row() >= self.rowCount():
         return False

      node = self.mCluster[index.row()]
      if node is None:
         return False

      if role == QtCore.Qt.EditRole:
         new_name = unicode(value.toString())
         self.mUndoStack.push(ChangeNodeName(node, new_name, self))

      self.emit(
         QtCore.SIGNAL("dataChanged(const QModelIndex &, const QModelIndex &)"),
         index, index
      )

      return True

   def rowCount(self, parent = QtCore.QModelIndex()):
      """ Returns the number of nodes in the current cluster configuration.
      """
      # If the parent is not valid, then we have no children.
      if parent.isValid():
         return 0
      else:
         return len(self.mCluster)

class ClusterSettingsChangeHandler(QtCore.QObject):
   def __init__(self, cluster, settings, parent = None):
      QtCore.QObject.__init__(self, parent)

      self.mCluster  = cluster
      self.mSettings = settings

      self.connect(self.mCluster, QtCore.SIGNAL("nodeAdded(QObject*)"),
                   self.onNodeAdded)
      self.connect(self.mCluster, QtCore.SIGNAL("nodeInserted(int,QObject*)"),
                   self.onNodeInserted)
      self.connect(self.mCluster, QtCore.SIGNAL("nodeRemoved(int,QObject*)"),
                   self.onNodeRemoved)
      self.connect(self.mCluster, QtCore.SIGNAL("nodesSorted()"),
                   self.onNodesSorted)
      self.connect(self.mCluster,
                   QtCore.SIGNAL("nodeChanged(int,QObject*,QObject*"),
                   self.onNodeChanged)

   def disconnectSlots(self):
      self.disconnect(self.mCluster, QtCore.SIGNAL("nodeAdded(QObject*)"),
                      self.onNodeAdded)
      self.disconnect(self.mCluster,
                      QtCore.SIGNAL("nodeInserted(int,QObject*)"),
                      self.onNodeInserted)
      self.disconnect(self.mCluster,
                      QtCore.SIGNAL("nodeRemoved(int,QObject*)"),
                      self.onNodeRemoved)
      self.disconnect(self.mCluster, QtCore.SIGNAL("nodesSorted()"),
                      self.onNodesSorted)
      self.disconnect(self.mCluster,
                      QtCore.SIGNAL("nodeChanged(int,QObject*,QObject*"),
                      self.onNodeChanged)

   def onNodeAdded(self, node):
      self.onNodeInserted(len(self.mCluster) - 1, node)

   def onNodeInserted(self, index, node):
      node_settings = \
         gui.util.settings.Setting(name = 'node',
                                   attributes = self._makeNodeAttrs(node))
      self.mSettings.insertChild(index, node_settings)

   def onNodeRemoved(self, index, node):
      self.mSettings.removeChild(index)

   def onNodesSorted(self):
      pass

   def onNodeChanged(self, index, oldNode, newNode):
      node_settings = self.mSettings.getChildAtIndex(index)
      for k, v in self._makeNodeAttrs(newNode):
         node_settings[k] = v

   def _makeNodeAttrs(self, node):
      return { 'name'    : node.getName(),
               'address' : node.getAddress() or "",
               'port'    : str(node.getPort()) or "0"
             }

def makeWindowAttrs(window):
   return { 'width'         : str(window.getWidth()),
            'height'        : str(window.getHeight()),
            'x'             : str(window.getX()),
            'y'             : str(window.getX()),
            'full_screen'   : str(window.isFullScreen()),
            'always_on_top' : str(window.isAlwaysOnTop()),
            'stereo'        : str(window.isStereo()),
            'border'        : str(window.hasBorder())
          }

class NodeSettingsChangeHandler(QtCore.QObject):
   def __init__(self, node, settings, parent = None):
      QtCore.QObject.__init__(self, parent)

      self.mNode     = node
      self.mSettings = settings

      self.mPipeSettings = settings.getChild('pipes')

      if self.mPipeSettings is None:
         self.mPipeSettings = gui.util.settings.Setting(name = 'pipes')
         self.mSettings.addChild(self.mPipeSettings)

      self.connect(self.mNode, QtCore.SIGNAL("nameChanged()"),
                   self.onNameChanged)
      self.connect(self.mNode, QtCore.SIGNAL("addressChanged()"),
                   self.onAddressChanged)
      self.connect(self.mNode, QtCore.SIGNAL("portChanged()"),
                   self.onPortChanged)

      pipes = self.mNode.getPipes()
      self.connect(pipes, QtCore.SIGNAL("pipeAdded(QObject*)"),
                   self.onPipeAdded)
      self.connect(pipes, QtCore.SIGNAL("pipeInserted(int,QObject*)"),
                   self.onPipeInserted)
      self.connect(pipes, QtCore.SIGNAL("pipeRemoved(int,QObject*)"),
                   self.onPipeRemoved)
      self.connect(pipes, QtCore.SIGNAL("pipeSorted()"), self.onPipesSorted)
      self.connect(pipes, QtCore.SIGNAL("pipeChanged(int,QObject*,QObject*)"),
                   self.onPipeChanged)

   def disconnectSlots(self):
      self.disconnect(self.mNode, QtCore.SIGNAL("nameChanged()"),
                      self.onNameChanged)
      self.disconnect(self.mNode, QtCore.SIGNAL("addressChanged()"),
                      self.onAddressChanged)
      self.disconnect(self.mNode, QtCore.SIGNAL("portChanged()"),
                      self.onPortChanged)

      pipes = self.mNode.getPipes()
      self.disconnect(pipes, QtCore.SIGNAL("pipeAdded(QObject*)"),
                      self.onPipeAdded)
      self.disconnect(pipes, QtCore.SIGNAL("pipeInserted(int,QObject*)"),
                      self.onPipeInserted)
      self.disconnect(pipes, QtCore.SIGNAL("pipeRemoved(int,QObject*)"),
                      self.onPipeRemoved)
      self.disconnect(pipes, QtCore.SIGNAL("pipeSorted()"), self.onPipesSorted)
      self.disconnect(pipes,
                      QtCore.SIGNAL("pipeChanged(int,QObject*,QObject*)"),
                      self.onPipeChanged)

   def onNameChanged(self):
      self.mSettings['name'] = self.mNode.getName()

   def onAddressChanged(self):
      self.mSettings['address'] = self.mNode.getAddress()

   def onPortChanged(self):
      self.mSettings['port'] = self.mNode.getPort()

   def onPipeAdded(self, pipe):
      self.onPipeInserted(len(self.mNode.getPipes()) - 1, pipe)

   def onPipeInserted(self, index, pipe):
      pipe_settings = \
         gui.util.settings.Setting(name = 'pipe',
                                   attributes = self._makePipeAttrs(pipe))
      self.mPipeSettings.insertChild(index, pipe_settings)

      if pipe.hasWindow():
         window_child = pipe_settings.getChild('window')
         if window_child is None:
            win_attrs    = makeWindowAttrs(pipe.getWindow())
            window_child = gui.util.settings.Setting(name = 'window',
                                                     attributes = win_attrs)
            pipe_settings.addChild(window_child)
         else:
            for k, v in makeWindowAttrs(pipe.getWindow()):
               window_child[k] = v
      else:
         window_child = pipe_settings.getChild('window')
         if window_child is not None:
            pipe_settings.removeChild(window_child)

      # Remove all old projector information.
      pipe_children = pipe_settings.getChildren()
      for c in pipe_children:
         if c.getName().endswith("_projector"):
            pipe_settings.removeChild(c)

      if pipe.hasProjector():
         projector  = pipe.getProjector()
         proj_attrs = projector.getAttributes()
         proj_child = gui.util.settings.Setting(name = projector.getType(),
                                                attributes = proj_attrs)
         pipe_settings.addChild(proj_child)

   def onPipeRemoved(self, index, pipe):
      self.mPipeSettings.removeChild(index)

   def onPipesSorted(self):
      pass

   def onPipeChanged(self, index, oldPipe, newPipe):
      pipe_settings = self.mPipeSettings.getChildAtIndex(index)
      for k, v in self._makePipeAttrs(newPipe):
         pipe_settings[k] = v

      if newPipe.hasWindow():
         window_setting = pipe_setting.getChild('window')
         for k, v in makeWindowAttrs(pipe.getWindow()).iteritems():
            window_setting[k] = v

      if newPipe.hasProjector():
         projector    = newPipe.getProjector()
         proj_setting = pipe_setting.getChild(projector.getType())
         for k, v in projector.getAttributes().iteritems():
            proj_setting[k] = v

   def _makePipeAttrs(self, pipe):
      return { 'name'   : pipe.getName(),
               'screen' : pipe.getScreenID()
             }

class PipeSettingsChangeHandler(QtCore.QObject):
   def __init__(self, pipe, settings, parent = None):
      QtCore.QObject.__init__(self, parent)

      self.mPipe                   = pipe
      self.mSettings               = settings
      self.mWindowChangeHandler    = None
      self.mProjectorChangeHandler = None

      if pipe.hasWindow():
         win_settings = settings.getChild('window')
         self.mWindowChangeHandler = \
            WindowSettingsChangeHandler(pipe.getWindow(), win_settings)

      if pipe.hasProjector():
         projector     = pipe.getProjector()
         proj_settings = settings.getChild(projector.getType())
         self.mProjectorChangeHandler = \
            projector.makeSettingsChangeHandler(proj_settings)

      self.connect(self.mPipe, QtCore.SIGNAL("nameChanged(QString*)"),
                   self.onNameChanged)
      self.connect(self.mPipe, QtCore.SIGNAL("screenIDChanged(QString*)"),
                   self.onScreenIDChanged)
      self.connect(self.mPipe,
                   QtCore.SIGNAL("windowChanged(QObject*,QObject*)"),
                   self.onWindowChanged)
      self.connect(self.mPipe,
                   QtCore.SIGNAL("projectorChanged(QObject*,QObject*)"),
                   self.onProjectorChanged)

   def disconnectSlots(self):
      if self.mWindowChangeHandler is not None:
         self.mWindowChangeHandler.disconnectSlots()
         self.mWindowChangeHandler = None

      if self.mProjectorChangeHandler is not None:
         self.mProjectorChangeHandler.disconnectSlots()
         self.mProjectorChangeHandler = None

      self.disconnect(self.mPipe, QtCore.SIGNAL("nameChanged(QString*)"),
                      self.onNameChanged)
      self.disconnect(self.mPipe, QtCore.SIGNAL("screenIDChanged(QString*)"),
                      self.onScreenIDChanged)
      self.disconnect(self.mPipe,
                      QtCore.SIGNAL("windowChanged(QObject*,QObject*)"),
                      self.onWindowChanged)
      self.disconnect(self.mPipe,
                      QtCore.SIGNAL("projectorChanged(QObject*,QObject*)"),
                      self.onProjectorChanged)

   def onNameChanged(self, newName):
      self.mSettings['name'] = str(newName)

   def onScreenIDChanged(self, newID):
      self.mSettings['screen'] = str(newID)

   def onWindowChanged(self, oldWindow, newWindow):
      # We did not have a window before, but we do now.
      if oldWindow is None:
         assert self.mWindowChangeHandler is None

         window_settings = \
            guil.util.settings.Setting(name = 'window',
                                       attributes = makeWindowAttrs(newWindow))
         self.mSettings.addChild(window_settings)
      # We had a window before, but now we do not.
      elif newWindow is None:
         assert self.mWindowChangeHandler is not None

         self.mSettings.removeChild('window')
         self.mWindowChangeHandler.disconnectSlots()
         self.mWindowChangeHandler = None
      # Both oldWindow and newWindow are not None.
      else:
         window_settings = self.mSettings.getChild('window')
         for k, v in makeWindowAttrs(newWindow):
            window_settings[k] = v

         self.mWindowChangeHandler.disconnectSlots()

      if newWindow is not None:
         assert window_settings is not None

         self.mWindowChangeHandler = \
            WindowSettingsChangeHandler(newWindow, window_settings)

   def onProjectorChanged(self, oldProjector, newProjector):
      # We did not have a projector before, but we do now.
      if oldProjector is None:
         assert self.mProjectorChangeHandler is None

         proj_settings = \
            gui.util.settings.Setting(name = newProjector.getType(),
                                      attributes = newProjector.getAttributes())
         self.mSettings.addChild(proj_settings)
      # We had a projector before, but now we do not.
      elif newProjector is None:
         assert self.mProjectorChangeHandler is not None

         self.mSettings.removeChild(oldProjector.getType())
         self.mProjectorChangeHandler.disconnectSlots()
         self.mProjectorChangeHandler = None
      # Both oldProjector and newProjector are not None.
      else:
         proj_settings = self.mSettings.getChild(oldProjector.getType())

         if oldProjector.getType() == newProjector.getType():
            if proj_settings is not None:
               for k, v in projector.getAttributes():
                  proj_settings[k] = v
         else:
            self.mSettings.removeChild(proj_settings)
            proj_settings = \
               guil.util.settings.Setting(name = newProjector.getType(),
                                          attributes = newProjector.getAttributes())
            self.mSettings.addChild(proj_settings)

         self.mProjectorChangeHandler.disconnectSlots()

      if newProjector is not None:
         assert proj_settings is not None

         self.mProjectorChangeHandler = \
            newProjector.makeSettingsChangeHandler(proj_settings)

class WindowSettingsChangeHandler(QtCore.QObject):
   def __init__(self, window, settings, parent = None):
      QtCore.QObject.__init__(self, parent)

      self.mWindow   = window
      self.mSettings = settings

      self.mIntPropSlots = [(QtCore.SIGNAL("widthChanged(int)"),
                             lambda v: self.onIntPropChanged('width', v)),
                            (QtCore.SIGNAL("heightChanged(int)"),
                             lambda v: self.onIntPropChanged('height', v)),
                            (QtCore.SIGNAL("positionXChanged(int)"),
                             lambda v: self.onIntPropChanged('x', v)),
                            (QtCore.SIGNAL("positionYChanged(int)"),
                             lambda v: self.onIntPropChanged('y', v))
                           ]

      self.mBoolPropSlots = \
         [(QtCore.SIGNAL("fullScreenChanged(bool)"),
           lambda v: self.onBoolPropChanged('full_screen', v)),
          (QtCore.SIGNAL("alwaysOnTopChanged(bool)"),
           lambda v: self.onBoolPropChanged('always_on_top', v)),
          (QtCore.SIGNAL("stereoChanged(bool)"),
           lambda v: self.onBoolPropChanged('stereo', v)),
          (QtCore.SIGNAL("borderChanged(bool)"),
           lambda v: self.onBoolPropChanged('border', v))
         ]

      for s in self.mIntPropSlots + self.mBoolPropSlots:
         self.connect(self.mWindow, s[0], s[1])

   def disconnectSlots(self):
      for s in self.mIntPropSlots + self.mBoolPropSlots:
         self.disconnect(self.mWindow, s[0], s[1])

   def onIntPropChanged(self, key, value):
      self.mSettings[key] = value

   def onBoolPropChanged(self, key, value):
      self.mSettings[key] = value

class PipeModel(QtCore.QAbstractListModel):
   def __init__(self, pipes, undoStack, parent = None):
      QtCore.QAbstractListModel.__init__(self, parent)

      self.mPipes     = pipes
      self.mUndoStack = undoStack

   def addPipe(self, newPipe):
      self.insertPipe(len(self.mPipes), newPipe)

   def insertPipe(self, row, newPipe):
      assert row >= 0

      self.beginInsertRows(QtCore.QModelIndex(), row, row)
      self.mPipes.insert(row, newPipe)
      self.endInsertRows()

   def removePipe(self, rowOrPipe):
      if rowOrPipe is None:
         return

      if type(rowOrPipe) == types.IntType:
         row  = rowOrPipe
         pipe = self.mPipes[row]
      else:
         pipe = rowOrPipe
         row  = self.mPipes.index(pipe)

      self.mPipeSettings.removeChild(row)

      self.beginRemoveRows(QtCore.QModelIndex(), row, row)
      del self.mPipes[row]
      self.endRemoveRows()

   def pipeChanged(self, pipe):
      if pipe in self.mPipes:
         row = self.mPipes.index(pipe)
         changed_index = self.index(row)

         self.emit(
            QtCore.SIGNAL("dataChanged(const QModelIndex &, const QModelIndex &)"),
            changed_index, changed_index
         )

   def onPipesChanged(self):
      self.emit(QtCore.SIGNAL("modelReset()"))

   def flags(self, index):
      default_flags  = QtCore.QAbstractListModel.flags(self, index)
      default_flags |= QtCore.Qt.ItemIsEditable

      if index.isValid():
         return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | \
                   default_flags
      else:
         return QtCore.Qt.ItemIsDropEnabled | default_flags

   def data(self, index, role = QtCore.Qt.DisplayRole):
      if index.isValid():
         pipe = self.mPipes[index.row()]

         if role == QtCore.Qt.UserRole:
            return pipe

         if pipe is not None:
            if role == QtCore.Qt.DecorationRole:
               return QtCore.QVariant()
            elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
               return QtCore.QVariant(str(pipe.getName()))

      return QtCore.QVariant()

   def setData(self, index, value, role):
      if not index.isValid() or index.row() >= self.rowCount():
         return False

      pipe = self.mPipes[index.row()]
      if pipe is None:
         return False

      if role == QtCore.Qt.EditRole:
         new_name = unicode(value.toString())
         self.mUndoStack.push(ChangePipeName(pipe, new_name, self))

      self.emit(
         QtCore.SIGNAL("dataChanged(const QModelIndex &, const QModelIndex &)"),
         index, index
      )

      return True

   def rowCount(self, parent = QtCore.QModelIndex()):
      """
      Returns the number of pipes in the current pipe list.
      """
      # If the parent is not valid, then we have no children.
      if parent.isValid():
         return 0
      else:
         return len(self.mPipes)

NODE_CREATE_ID            = 10
PIPE_CREATE_ID            = 20
PROJECTOR_CHANGE_PARAM_ID = 30
CHANGE_BACKUP_INTERVAL_ID = 40

class ChangeBackupInterval(QtGui.QUndoCommand):
   def __init__(self, settings, widget, value, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Backup Interval Change', parent)
      self.mSettings = settings
      self.mWidget   = widget
      self.mValue    = value

      default_backup_interval = 60
      self.mOldValue = self.mSettings.getValue('general/backup_interval', default_backup_interval)

   def id(self):
      return CHANGE_BACKUP_INTERVAL_ID 

   def mergeWith(self, command):
      if command.id() == self.id():
         self.mValue = command.mValue
         return True
      return False

   def redo(self):
      self.mSettings.set('general/backup_interval', str(self.mValue))

      # XXX: We would rather do this with an event interface on the SettingsDB.
      env = environ.Environment()
      env.curDocument.__class__.setBackupInterval(self.mValue * 1000)

   def undo(self):
      self.mSettings.set('general/backup_interval', str(self.mOldValue))

      # XXX: We would rather do this with an event interface on the SettingsDB.
      env = environ.Environment()
      env.curDocument.__class__.setBackupInterval(self.mOldValue * 1000)

      # Update the UI to reflect this since it was not caused by a change
      # to the widget itself.
      self.mWidget.blockSignals(True)
      self.mWidget.setValue(int(self.mOldValue))
      self.mWidget.blockSignals(False)

class ChangeRotationMultiplier(QtGui.QUndoCommand):
   def __init__(self, widget, settings, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Change Rotation Multiplier', parent)
      self.mWidget   = widget
      self.mSettings = settings
      self.mNewValue = str(widget.text())
      self.mOldValue = settings['navigation/rot_mult'].value.strip()

   def redo(self):
      rot_mult = self.mSettings['navigation/rot_mult']
      rot_mult.value = self.mNewValue
      self.mWidget.setText(self.mNewValue)

   def undo(self):
      self.mWidget.setText(self.mOldValue)

class ChangeProtocolType(QtGui.QUndoCommand):
   def __init__(self, comboBox, settings, type, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Change Network Protocol', parent)
      self.mComboBox = comboBox
      self.mSettings = settings
      self.mType     = type
      self.mOldType  = settings['network/protocol']['type']

   def redo(self):
      protocol = self.mSettings['network/protocol']
      protocol['type'] = self.mType
      self._updateComboBox(self.mType)

   def undo(self):
      protocol = self.mSettings['network/protocol']
      protocol['type'] = self.mOldType
      self._updateComboBox(self.mOldType)

   def _updateComboBox(self, type):
      i = 0
      while i < self.mComboBox.count():
         t = str(self.mComboBox.itemData(i).toString())

         if t == type:
            if i != self.mComboBox.currentIndex():
               self.mComboBox.blockSignals(True)
               self.mComboBox.setCurrentIndex(i)
               self.mComboBox.blockSignals(False)
            break

         i += 1

class AddNode(QtGui.QUndoCommand):
   def __init__(self, model, theCluster, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Node Addition', parent)
      self.mModel   = model
      self.mCluster = theCluster
      self.mNode    = gui.cluster.node.Node("New Node")
      self.mMerged  = None

   def id(self):
      return NODE_CREATE_ID

   def mergeWith(self, command):
      '''
      Determines if the given command can be merged with this command. This
      is allows if the command is a ChangeNodeName instance. In that case, we
      treat the two-step process of creating a node and setting its initial
      name as a single command.
      '''
      result = False

      if command.id() == self.id():
         # We only merge the given command if we do not already have a merged
         # command.
         if self.mMerged is None and command.__class__ == ChangeNodeName:
            self.mMerged = command
            result = True

      return result

   def redo(self):
      self.mIndex = len(self.mCluster)
      self.mModel.addNode(self.mNode)

      if self.mMerged is not None:
         self.mMerged.redo()

   def undo(self):
      if self.mMerged is not None:
         self.mMerged.undo()

      self.mModel.removeNode(self.mIndex)

class RemoveNode(QtGui.QUndoCommand):
   def __init__(self, model, index, node, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Node Removal', parent)
      self.mClusterModel = model
      self.mIndex        = index
      self.mOldNode      = node

   def redo(self):
      assert self.mOldNode is not None
      self.mClusterModel.removeNode(self.mIndex)

   def undo(self):
      assert self.mOldNode is not None
      self.mClusterModel.insertNode(self.mIndex, self.mOldNode)

class MoveNode(QtGui.QUndoCommand):
   def __init__(self, model, curIndex, newIndex, node, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Node Ordering', parent)
      self.mClusterModel = model
      self.mOldIndex     = curIndex
      self.mNewIndex     = newIndex
      self.mNode         = node

   def redo(self):
      self.mClusterModel.removeNode(self.mOldIndex)
      self.mClusterModel.insertNode(self.mNewIndex, self.mNode)

   def undo(self):
      self.mClusterModel.removeNode(self.mNewIndex)
      self.mClusterModel.insertNode(self.mOldIndex, self.mNode)

class ChangeNodeName(QtGui.QUndoCommand):
   def __init__(self, node, name, model, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Change Node Name', parent)
      self.mNode         = node
      self.mName         = name
      self.mClusterModel = model
      self.mOldName      = node.getName()

   def id(self):
      # This returns NODE_CREATE_ID because setting the initial name for a
      # newly created node is treated as part of the creation process. See
      # AddNode for more information.
      return NODE_CREATE_ID

   def redo(self):
      self.mNode.setName(self.mName)
      self.mClusterModel.nodeChanged(self.mNode)

   def undo(self):
      self.mNode.setName(self.mOldName)
      self.mClusterModel.nodeChanged(self.mNode)

class ChangeNodeAddress(QtGui.QUndoCommand):
   def __init__(self, editor, node, addr, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Change Node Address', parent)
      self.mEditor   = editor
      self.mNode     = node
      self.mAddr     = addr
      self.mOldAddr  = node.getAddress()

   def redo(self):
      self.mNode.setAddress(self.mAddr)
      self._updateEditor(self.mAddr)

   def undo(self):
      self.mNode.setAddress(self.mOldAddr)
      self._updateEditor(self.mOldAddr)

   def _updateEditor(self, addr):
      if self.mEditor.text() != addr:
         self.mEditor.blockSignals(True)
         self.mEditor.setText(addr)
         self.mEditor.blockSignals(False)

class ChangeNodePort(QtGui.QUndoCommand):
   def __init__(self, editor, node, port, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Change Node Address', parent)
      self.mEditor   = editor
      self.mNode     = node
      self.mPort     = port
      self.mOldPort  = node.getPort()

   def redo(self):
      self.mNode.setPort(self.mPort)
      self._updateEditor(self.mPort)

   def undo(self):
      self.mNode.setPort(self.mOldPort)
      self._updateEditor(self.mOldPort)

   def _updateEditor(self, port):
      if self.mEditor.value() != port:
         self.mEditor.blockSignals(True)
         self.mEditor.setValue(port)
         self.mEditor.blockSignals(False)

class AddPipe(QtGui.QUndoCommand):
   def __init__(self, model, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Pipe Addition', parent)
      self.mPipeModel = model
      self.mPipe      = gui.cluster.display.Pipe("New Pipe")
      self.mMerged    = None

      self.mPipe.setWindow(gui.cluster.display.Window(self.mPipe))

   def id(self):
      return PIPE_CREATE_ID

   def mergeWith(self, command):
      '''
      Determines if the given command can be merged with this command. This
      is allows if the command is a ChangePipeName instance. In that case, we
      treat the two-step process of creating a pipe and setting its initial
      name as a single command.
      '''
      result = False

      if command.id() == self.id():
         # We only merge the given command if we do not already have a merged
         # command.
         if self.mMerged is None and command.__class__ == ChangePipeName:
            self.mMerged = command
            result = True

      return result

   def redo(self):
      self.mIndex = self.mPipeModel.rowCount()
      self.mPipeModel.addPipe(self.mPipe)

      if self.mMerged is not None:
         self.mMerged.redo()

   def undo(self):
      if self.mMerged is not None:
         self.mMerged.undo()

      self.mPipeModel.removePipe(self.mIndex)

class RemovePipe(QtGui.QUndoCommand):
   def __init__(self, model, index, pipe, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Pipe Removal', parent)
      self.mPipeModel = model
      self.mIndex     = index
      self.mPipe      = pipe

   def redo(self):
      assert self.mPipe is not None
      self.mPipeModel.removePipe(self.mIndex)

   def undo(self):
      assert self.mPipe is not None
      self.mPipeModel.insertPipe(self.mIndex, self.mPipe)

class MovePipe(QtGui.QUndoCommand):
   def __init__(self, model, curIndex, newIndex, pipe, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Pipe Ordering', parent)
      self.mPipeModel = model
      self.mOldIndex  = curIndex
      self.mNewIndex  = newIndex
      self.mPipe      = pipe

   def redo(self):
      self.mPipeModel.removePipe(self.mOldIndex)
      self.mPipeModel.insertPipe(self.mNewIndex, self.mPipe)

   def undo(self):
      self.mPipeModel.removePipe(self.mNewIndex)
      self.mPipeModel.insertPipe(self.mOldIndex, self.mPipe)

class ChangePipeName(QtGui.QUndoCommand):
   def __init__(self, pipe, name, model, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Change Pipe Name', parent)
      self.mPipe      = pipe
      self.mName      = name
      self.mPipeModel = model
      self.mOldName   = pipe.getName()

   def id(self):
      # This returns PIPE_CREATE_ID because setting the initial name for a
      # newly created pipe is treated as part of the creation process. See
      # AddPipe for more information.
      return PIPE_CREATE_ID

   def redo(self):
      self.mPipe.setName(self.mName)
      self.mPipeModel.pipeChanged(self.mPipe)

   def undo(self):
      self.mPipe.setName(self.mOldName)
      self.mPipeModel.pipeChanged(self.mPipe)

class ChangePipeScreen(QtGui.QUndoCommand):
   def __init__(self, editor, pipe, screen, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Change Pipe Screen', parent)
      self.mEditor    = editor
      self.mPipe      = pipe
      self.mScreen    = screen
      self.mOldScreen = pipe.getScreenID()

   def redo(self):
      self.mPipe.setScreenID(self.mScreen)
      self._updateEditor(self.mScreen)

   def undo(self):
      self.mPipe.setScreenID(self.mOldScreen)
      self._updateEditor(self.mOldScreen)

   def _updateEditor(self, screen):
      if self.mEditor.text() != screen:
         self.mEditor.blockSignals(True)
         self.mEditor.setText(screen)
         self.mEditor.blockSignals(False)

class ToggleWindowProperty(QtGui.QUndoCommand):
   def __init__(self, checkBox, changeFunc, state, desc, parent = None):
      QtGui.QUndoCommand.__init__(self, desc, parent)
      self.mCheckBox   = checkBox
      self.mChangeFunc = changeFunc
      self.mState      = state

   def redo(self):
      self.mChangeFunc(self.mState)
      self._updateCheckBox(self.mState)

   def undo(self):
      self.mChangeFunc(not self.mState)
      self._updateCheckBox(not self.mState)

   def _updateCheckBox(self, state):
      cur_state = self.mCheckBox.checkState()

      if state and cur_state != QtCore.Qt.Checked:
         self.mCheckBox.blockSignals(True)
         self.mCheckBox.setCheckState(QtCore.Qt.Checked)
         self.mCheckBox.blockSignals(False)
      elif not state and cur_state != QtCore.Qt.Unchecked:
         self.mCheckBox.blockSignals(True)
         self.mCheckBox.setCheckState(QtCore.Qt.Unchecked)
         self.mCheckBox.blockSignals(False)

class ChangeWindowProperty(QtGui.QUndoCommand):
   def __init__(self, editor, changeFunc, newValue, oldValue, desc,
                parent = None):
      QtGui.QUndoCommand.__init__(self, desc, parent)
      self.mEditor     = editor
      self.mChangeFunc = changeFunc
      self.mNewValue   = newValue
      self.mOldValue   = oldValue

   def redo(self):
      self.mChangeFunc(self.mNewValue)
      self._updateEditor(self.mNewValue)

   def undo(self):
      self.mChangeFunc(self.mOldValue)
      self._updateEditor(self.mOldValue)

   def _updateEditor(self, value):
      if str(value) != self.mEditor.text():
         self.mEditor.blockSignals(True)
         self.mEditor.setText(str(value))
         self.mEditor.blockSignals(False)

class ChangeProjectorType(QtGui.QUndoCommand):
   def __init__(self, comboBox, type, parent = None):
      QtGui.QUndoCommand.__init__(self, 'Change Projector Type', parent)
      self.mComboBox = comboBox
      self.mType     = type
      self.mPipe     = pipe
      self.mOldType  = None

      for t in projector_types:
         if pipeSettings.getChild(t) is not None:
            self.mOldType = t
            break

      assert self.mOldType is not None

   def redo(self):
      self._updateComboBox(self.mType)

   def undo(self):
      self._updateComboBox(self.mOldType)

   def _updateComboBox(self, type):
      i = 0
      while i < self.mComboBox.count():
         t = str(self.mComboBox.itemData(i).toString())

         if t == type:
            if i != self.mComboBox.currentIndex():
               self.mComboBox.blockSignals(True)
               self.mComboBox.setCurrentIndex(i)
               self.mComboBox.blockSignals(False)
            break

         i += 1

class ChangeProjectorParam(QtGui.QUndoCommand):
   def __init__(self, tableModel, projector, index, value, role, desc,
                parent = None):
      QtGui.QUndoCommand.__init__(self, "Set %s" % desc, parent)
      self.mTableModel = tableModel
      self.mProjector  = projector
      self.mIndex      = index
      self.mValue      = value
      self.mRole       = role
      self.mOldValue   = projector.data(index)

   def redo(self):
      if self.mProjector.setData(self.mIndex, self.mValue, self.mRole):
         self._emitChangeSignal()

   def undo(self):
      if self.mProjector.setData(self.mIndex, self.mOldValue, self.mRole):
         self._emitChangeSignal()

   def _emitChangeSignal(self):
      self.mTableModel.emit(
         QtCore.SIGNAL("dataChanged(QModelIndex,QModelIndex)"), self.mIndex,
         self.mIndex
      )
      self.mTableModel.emit(QtCore.SIGNAL("dataChanged(int)"),
                            self.mIndex.row())
